import { Peer, Encryption, sha256 } from './index.js'
import { PeerWorkerProxy } from './proxy.js'
import { sodium } from '../crypto.js'
import { Buffer } from '../buffer.js'
import { isBufferLike } from '../util.js'
import { Packet, CACHE_TTL } from './packets.js'

/**
 * Initializes and returns the network bus.
 *
 * @async
 * @function
 * @param {object} options - Configuration options for the network bus.
 * @param {object} events - A nodejs compatibe implementation of the events module.
 * @param {object} dgram - A nodejs compatible implementation of the dgram module.
 * @returns {Promise<events.EventEmitter>} - A promise that resolves to the initialized network bus.
 */
async function api (options = {}, events, dgram) {
  await sodium.ready
  const bus = new events.EventEmitter()
  bus._on = bus.on
  bus._once = bus.once
  bus._emit = bus.emit

  if (!options.indexed) {
    if (!options.clusterId && !options.config?.clusterId) {
      throw new Error('expected options.clusterId')
    }

    if (typeof options.signingKeys !== 'object') throw new Error('expected options.signingKeys to be of type Object')
    if (options.signingKeys.publicKey?.constructor.name !== 'Uint8Array') throw new Error('expected options.signingKeys.publicKey to be of type Uint8Array')
    if (options.signingKeys.privateKey?.constructor.name !== 'Uint8Array') throw new Error('expected options.signingKeys.privateKey to be of type Uint8Array')
  }

  let clusterId = bus.clusterId = options.clusterId || options.config?.clusterId

  if (clusterId) clusterId = Buffer.from(clusterId) // some peers don't have clusters

  let useWorker = globalThis.isSocketRuntime
  if (options.worker === false) useWorker = false

  const Ctor = useWorker ? PeerWorkerProxy : Peer
  const _peer = new Ctor(options, dgram)

  _peer.onJoin = (packet, ...args) => {
    packet = Packet.from(packet)
    if (packet.clusterId.compare(clusterId) !== 0) return
    bus._emit('#join', packet, ...args)
  }

  _peer.onPacket = (packet, ...args) => {
    packet = Packet.from(packet)
    if (packet.clusterId.compare(clusterId) !== 0) return
    bus._emit('#packet', packet, ...args)
  }

  _peer.onStream = (packet, ...args) => {
    packet = Packet.from(packet)
    if (packet.clusterId.compare(clusterId) !== 0) return
    bus._emit('#stream', packet, ...args)
  }

  _peer.onData = (...args) => bus._emit('#data', ...args)
  _peer.onDebug = (...args) => bus._emit('#debug', ...args)
  _peer.onSend = (...args) => bus._emit('#send', ...args)
  _peer.onFirewall = (...args) => bus._emit('#firewall', ...args)
  _peer.onMulticast = (...args) => bus._emit('#multicast', ...args)
  _peer.onSync = (...args) => bus._emit('#sync', ...args)
  _peer.onSyncStart = (...args) => bus._emit('#sync-start', ...args)
  _peer.onSyncEnd = (...args) => bus._emit('#sync-end', ...args)
  _peer.onDisconnection = (...args) => bus._emit('#disconnection', ...args)
  _peer.onQuery = (...args) => bus._emit('#query', ...args)
  _peer.onNat = (...args) => bus._emit('#network-change', ...args)
  _peer.onWarn = (...args) => bus._emit('#warning', ...args)
  _peer.onState = (...args) => bus._emit('#state', ...args)
  _peer.onConnecting = (...args) => bus._emit('#connecting', ...args)
  _peer.onConnection = (...args) => bus._emit('#connection', ...args)

  // TODO check if its not a network error
  _peer.onError = (...args) => bus._emit('#error', ...args)

  _peer.onReady = info => {
    Object.assign(_peer, {
      isReady: true,
      ...info
    })

    bus._emit('#ready', info)
  }

  bus.subclusters = new Map()

  /**
   * Gets general, read only information of the network peer.
   *
   * @function
   * @returns {object} - The general information.
   */
  bus.getInfo = () => _peer.getInfo()
  bus.getMetrics = () => _peer.getMetrics()

  /**
   * Gets the read only state of the network peer.
   *
   * @function
   * @returns {object} - The address information.
   */
  bus.getState = () => _peer.getState()

  /**
   * Indexes a new peer in the network.
   *
   * @function
   * @param {object} params - Peer information.
   * @param {string} params.peerId - The peer ID.
   * @param {string} params.address - The peer address.
   * @param {number} params.port - The peer port.
   * @throws {Error} - Throws an error if required parameters are missing.
   */
  bus.addIndexedPeer = ({ peerId, address, port }) => {
    return _peer.addIndexedPeer({ peerId, address, port })
  }

  bus.close = () => _peer.close()
  bus.sync = (peerId) => _peer.sync(peerId)
  bus.reconnect = () => _peer.reconnect()
  bus.disconnect = () => _peer.disconnect()

  bus.sealUnsigned = (m, v = options.signingKeys) => _peer.sealUnsigned(m, v)
  bus.openUnsigned = (m, v = options.signingKeys) => _peer.openUnsigned(m, v)

  bus.seal = (m, v = options.signingKeys) => _peer.seal(m, v)
  bus.open = (m, v = options.signingKeys) => _peer.open(m, v)

  bus.send = (...args) => _peer.send(...args)

  bus.query = (...args) => _peer.query(...args)

  bus.MAX_CACHE_TTL = CACHE_TTL

  const pack = async (eventName, value, opts = {}) => {
    if (typeof eventName !== 'string') throw new Error('event name must be a string')
    if (eventName.length === 0) throw new Error('event name too short')

    if (opts.ttl) opts.ttl = Math.min(opts.ttl, CACHE_TTL)

    const args = {
      clusterId,
      ...opts,
      usr1: await sha256(eventName, { bytes: true })
    }

    if (!isBufferLike(value) && typeof value === 'object') {
      try {
        args.message = Buffer.from(JSON.stringify(value))
      } catch (err) {
        return bus._emit('error', err)
      }
    } else {
      args.message = Buffer.from(value)
    }

    args.usr2 = Buffer.from(options.signingKeys.publicKey)
    args.sig = Encryption.sign(args.message, options.signingKeys.privateKey)

    return args
  }

  const unpack = async packet => {
    let verified
    const scid = Buffer.from(packet.subclusterId).toString('base64')
    const sub = bus.subclusters.get(scid)
    if (!sub) return {}

    const opened = await _peer.open(packet.message, scid)

    if (!opened) {
      sub._emit('unopened', { packet })
      return {}
    }

    if (packet.sig) {
      try {
        if (Encryption.verify(opened, packet.sig, packet.usr2)) {
          verified = true
        }
      } catch (err) {
        sub._emit('unverified', packet)
        return {}
      }
    }

    return { opened: Buffer.from(opened), verified }
  }

  /**
   * Publishes an event to the network bus.
   *
   * @async
   * @function
   * @param {string} eventName - The name of the event.
   * @param {any} value - The value associated with the event.
   * @param {object} opts - Additional options for publishing.
   * @returns {Promise<any>} - A promise that resolves to the published event details.
   */
  bus.emit = async (eventName, value, opts = {}) => {
    const args = await pack(eventName, value, opts)
    if (!options.sharedKey) {
      throw new Error('Can\'t emit to the top level cluster, a shared key was not provided in the constructor or the arguments options')
    }

    return await _peer.publish(options.sharedKey || opts.sharedKey, args)
  }

  bus.on = async (eventName, cb) => {
    if (eventName[0] !== '#') eventName = await sha256(eventName)
    bus._on(eventName, cb)
  }

  bus.subcluster = async (options = {}) => {
    if (!options.sharedKey?.constructor.name) {
      throw new Error('expected options.sharedKey to be of type Uint8Array')
    }

    const derivedKeys = await Encryption.createKeyPair(options.sharedKey)
    const subclusterId = Buffer.from(derivedKeys.publicKey)
    const scid = subclusterId.toString('base64')

    if (bus.subclusters.has(scid)) return bus.subclusters.get(scid)

    const sub = new events.EventEmitter()
    sub._emit = sub.emit
    sub._on = sub.on
    sub.peers = new Map()

    bus.subclusters.set(scid, sub)

    sub.peerId = _peer.peerId
    sub.subclusterId = subclusterId
    sub.sharedKey = options.sharedKey
    sub.derivedKeys = derivedKeys

    sub.stream = async (eventName, value, opts = {}) => {
      opts.clusterId = opts.clusterId || clusterId
      opts.subclusterId = opts.subclusterId || sub.subclusterId

      const args = await pack(eventName, value, opts)

      let packets

      for (const p of sub.peers.values()) {
        const result = await _peer.stream(p.peerId, sub.sharedKey, args)
        if (!packets) packets = result
      }
      return packets
    }

    sub.emit = async (eventName, value, opts = {}) => {
      opts.clusterId = opts.clusterId || clusterId
      opts.subclusterId = opts.subclusterId || sub.subclusterId

      const args = await pack(eventName, value, opts)

      if (sub.peers.values().length) {
        let packets = []

        for (const p of sub.peers.values()) {
          const r = await _peer.stream(p.peerId, sub.sharedKey, args)
          if (packets.length === 0) packets = r
        }

        for (const packet of packets) {
          const p = Packet.from(packet)
          const pid = packet.packetId.toString('hex')
          _peer.cache.insert(pid, p)

          _peer.unpublished[pid] = Date.now()
          if (!Peer.onLine()) continue

          _peer.mcast(packet)
        }

        const head = packets.find(p => p.index === 0)

        if (_peer.onPacket && head) { // try to emit a single packet
          const p = await _peer.cache.compose(head)
          _peer.onPacket(p, _peer.port, _peer.address, true)
          return [p]
        }

        return packets
      } else {
        const packets = await _peer.publish(sub.sharedKey, args)
        return packets
      }
    }

    sub.on = async (eventName, cb) => {
      if (eventName[0] !== '#') eventName = await sha256(eventName)
      sub._on(eventName, cb)
    }

    sub.off = async (eventName, fn) => {
      if (eventName[0] !== '#') eventName = await sha256(eventName)
      sub.removeListener(eventName, fn)
    }

    sub.join = () => _peer.join(sub.sharedKey, options)

    bus._on('#ready', () => {
      const scid = sub.subclusterId.toString('base64')
      const subcluster = bus.subclusters.get(scid)
      if (subcluster) _peer.join(subcluster.sharedKey, options)
    })

    _peer.join(sub.sharedKey, options)
    return sub
  }

  bus._on('#join', async (packet, peer) => {
    const scid = packet.subclusterId.toString('base64')
    const sub = bus.subclusters.get(scid)
    if (!sub) return
    if (!peer || !peer.peerId) return

    let ee = sub.peers.get(peer.peerId)

    if (!ee) {
      ee = new events.EventEmitter()

      ee._on = ee.on
      ee._emit = ee.emit

      ee.peerId = peer.peerId
      ee.address = peer.address
      ee.port = peer.port

      ee.emit = async (eventName, value, opts = {}) => {
        opts.clusterId = opts.clusterId || clusterId
        opts.subclusterId = opts.subclusterId || sub.subclusterId

        const args = await pack(eventName, value, opts)
        return _peer.stream(peer.peerId, sub.sharedKey, args)
      }

      ee.on = async (eventName, cb) => {
        if (eventName[0] !== '#') eventName = await sha256(eventName)
        ee._on(eventName, cb)
      }
    }

    const oldPeer = sub.peers.has(peer.peerId)
    const portChange = oldPeer.port !== peer.port
    const addressChange = oldPeer.address !== peer.address
    const natChange = oldPeer.natType !== peer.natType
    const change = portChange || addressChange || natChange

    ee._peer = peer

    sub.peers.set(peer.peerId, ee)
    const isStateChange = !oldPeer || change

    _peer.onDebug(_peer.peerId, `<-- API CONNECTION JOIN (scid=${scid}, peerId=${peer.peerId.slice(0, 6)})`)

    sub._emit('#join', ee, packet, isStateChange)
  })

  const handlePacket = async (packet, peer, port, address) => {
    const scid = packet.subclusterId.toString('base64')
    const sub = bus.subclusters.get(scid)
    if (!sub) return

    const eventName = packet.usr1.toString('hex')
    const { verified, opened } = await unpack(packet)
    if (verified) packet.verified = true

    sub._emit(eventName, opened, packet)

    const ee = sub.peers.get(packet.streamFrom || peer?.peerId)
    if (ee) ee._emit(eventName, opened, packet)
  }

  bus._on('#stream', handlePacket)
  bus._on('#packet', handlePacket)

  bus._on('#disconnection', peer => {
    for (const sub of [...bus.subclusters.values()]) {
      sub._emit('#leave', peer)
      sub.peers.delete(peer.peerId)
    }
  })

  await _peer.init()
  return bus
}

export { api }
export default api
